package org.mozhu.zufang.wechat.wx;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.mozhu.zufang.wechat.wx.exception.WechatException;
import org.mozhu.zufang.wechat.wx.oauth.OAuthAccessToken;
import org.mozhu.zufang.wechat.wx.oauth.OAuthUserInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Map;
import java.util.concurrent.ExecutionException;

@Service
@Slf4j
/**
 * Step 1: grant by authorization_code, AUTH_ACCESS_TOKEN_URL
 * Step 2: snsapi_base authorize to get access token & openid
 */
public class WechatService {
    private final static String BASE_URL = "https://api.weixin.qq.com";
    private final static String ACCESS_TOKEN_URL = BASE_URL + "/cgi-bin/token?grant_type=client_credential&appid=${appId}&secret=${appSecret}";

    private final static String AUTH_ACCESS_TOKEN_URL = BASE_URL + "/sns/oauth2/access_token?appid=${appId}&secret=${appSecret}&code=${code}&grant_type=authorization_code";
    private final static String AUTH_SNS_USERINFO = BASE_URL + "/sns/userinfo?access_token=${access_token}&openid=${openid}&lang=zh_CN";

    private final static String OPEN_OAUTH_BASE_URL = "https://open.weixin.qq.com/connect/oauth2";
    private final static String OPEN_OAUTH_AUTHORIZE_SNSAPI_BASE = OPEN_OAUTH_BASE_URL + "/authorize?appid=${appId}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_base";
    private final static String OPEN_OAUTH_AUTHORIZE_SNSAPI_USERINFO = OPEN_OAUTH_BASE_URL + "/authorize?appid=${appId}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_userinfo";


    @Autowired
    WechatProperties wechatProperties;

    private CloseableHttpClient client = HttpClients.custom().setDefaultCookieStore(new BasicCookieStore()).build();
    private final Cache<String, OAuthAccessToken> cache = CacheBuilder.from("expireAfterWrite=5m").build();


    public String getOpenId(String code) {
        try {
            return cache.get(code, () -> getOAuthAccessToken(code)).getOpenId();
        } catch (ExecutionException e) {
            throw new IllegalStateException("Get openid failed", e);
        }
    }

    public String getAccessToken(String code) {
        try {
            return cache.get(code, () -> getOAuthAccessToken(code)).getAccessToken();
        } catch (ExecutionException e) {
            throw new IllegalStateException("Get access token failed", e);
        }
    }

    public OAuthAccessToken getOAuthAccessToken(String code) {
        String appId = wechatProperties.getAppId();
        String appSecret = wechatProperties.getAppSecret();
        String accessTokenUrl = AUTH_ACCESS_TOKEN_URL.replace("${code}", code).replace("${appId}", appId).replace("${appSecret}", appSecret);
        try {
            CloseableHttpResponse response = client.execute(new HttpGet(accessTokenUrl));
            Map<String, Object> obj = new ObjectMapper().readValue(response.getEntity().getContent(), Map.class);
            verifyResponse(obj);
            String accessToken = (String) obj.get("access_token");
            String refreshToken = (String) obj.get("refresh_token");
            String openid = (String) obj.get("openid");
            Integer expires = (Integer) obj.get("expires_in");
            return OAuthAccessToken.builder().accessToken(accessToken).expires(expires).refreshToken(refreshToken).openId(openid).build();
        } catch (Exception e) {
            handleException("Get access token failed", e);
        }
        return null;
    }


    public AccessToken getAccessToken() {
        String appId = wechatProperties.getAppId();
        String appSecret = wechatProperties.getAppSecret();
        String accessTokenUrl = ACCESS_TOKEN_URL.replace("${appId}", appId).replace("${appSecret}", appSecret);
        try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
            CloseableHttpResponse response = client.execute(new HttpGet(accessTokenUrl));
            Map<String, Object> obj = new ObjectMapper().readValue(response.getEntity().getContent(), Map.class);
            verifyResponse(obj);
            String token = (String) obj.get("access_token");
            Integer expires = (Integer) obj.get("expires_in");
            return AccessToken.builder().token(token).expires(expires).build();
        } catch (Exception e) {
            handleException("Get access token failed", e);
        }
        return null;
    }

    private void handleException(String message, Exception e) {
        log.error(message, e);
        if (e instanceof WechatException) {
            throw (WechatException) e;
        } else {
            throw new IllegalStateException(message, e);
        }
    }

    private void verifyResponse(Map<String, Object> obj) {
        String errorCode = (String) obj.get("errcode");
        String errorMessage = (String) obj.get("errmsg");
        throw new WechatException(errorCode, errorMessage);
    }

    public String getUrlForCode(String redirectUri) {
        String appId = wechatProperties.getAppId();
        return OPEN_OAUTH_AUTHORIZE_SNSAPI_BASE.replace("${appId}", appId).replace("${redirectUri}", encodeUrl(redirectUri));
    }


    public String getUrlForProfile(String redirectUri) {
        String appId = wechatProperties.getAppId();
        return OPEN_OAUTH_AUTHORIZE_SNSAPI_USERINFO.replace("${appId}", appId).replace("${redirectUri}", encodeUrl(redirectUri));
    }

    private String encodeUrl(String url) {
        try {
            return URLEncoder.encode(url, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new IllegalStateException("UnsupportedEncodingException", e);
        }
    }

    public OAuthUserInfo getUserInfo(String code) {
        try {
            OAuthAccessToken authAccessToken = cache.get(code, () -> getOAuthAccessToken(code));
            String accessToken = authAccessToken.getAccessToken();
            String openId = authAccessToken.getOpenId();
            String userInfoUrl = AUTH_SNS_USERINFO.replace("${access_token}", accessToken).replace("${openid}", openId);

            CloseableHttpResponse response = client.execute(new HttpGet(userInfoUrl));
            Map<String, Object> obj = new ObjectMapper().readValue(response.getEntity().getContent(), Map.class);
            verifyResponse(obj);
            OAuthUserInfo.OAuthUserInfoBuilder builder = OAuthUserInfo.builder();
            builder.openId((String) obj.get("openid"));
            builder.nickName((String) obj.get("nickname"));
            builder.sex((String) obj.get("sex"));
            builder.province((String) obj.get("province"));
            builder.city((String) obj.get("city"));
            builder.country((String) obj.get("country"));
            builder.headImgUrl((String) obj.get("headimgurl"));
            builder.privilege((String) obj.get("privilege"));
            builder.unionid((String) obj.get("unionid"));
            return builder.build();
        } catch (Exception e) {
            handleException("Get user info failed", e);
        }
        return null;
    }

}
